package org.third.security.cert;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.util.Assert;

import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Obtains the principal from a certificate based on the X.509 attribute mentioned in the csa.properties file.
 */
public class CustomX509PrincipalExtractor implements X509PrincipalExtractor {

    private static final Logger logger = LoggerFactory.getLogger(CustomX509PrincipalExtractor.class);
    private static final String SUBJECTDN = "subjectDN";
    private static final String SUBJECT_ALT_NAME = "san";

    private Pattern userNameRegex = Pattern.compile("CN=(.*?),", Pattern.CASE_INSENSITIVE);
    
    String userExtractorExpression="CN=(.*?),";
    String x509Attribute = "subjectDN"; // subjectDN, SAN
    String sanType = SubjectAlternativeNameGeneralNames.rfc822Name.name();

    CustomX509UserPrincipalNameExtractor otherNameUserPrincipalNameExtractor;

    /**
     * Returns the principal (usually a String) for the given certificate.
     */
    @Override
    public Object extractPrincipal(X509Certificate clientCert) {
        // 1. The X.509 attribute that needs to be read for obtaining the principal name is mentioned
        // in the applicationContext.properties file and is passed an an argument to the extractor.
        //
        // 2. The X.509 attributes that are currently supported are namely subjectDN and subjectAltName.

        String userName = null;

        try {
            if (this.x509Attribute.equalsIgnoreCase(SUBJECTDN)) {
                String subjectDN = clientCert.getSubjectX500Principal().getName();

                logger.debug("The " + SUBJECTDN + " value is " + subjectDN);

                userName = getUserNameFromPrincipal(subjectDN);
            } else if (this.x509Attribute.equalsIgnoreCase(SUBJECT_ALT_NAME)) {
                if (clientCert.getSubjectAlternativeNames() != null) {
                    userName = getSubjectAlternativeName(clientCert.getSubjectAlternativeNames());
                } else {
                    logger.info(
                            "The " + SUBJECT_ALT_NAME + " subjectAlternativeNames is null, no user name can be determined: " + clientCert);
                }
            } else {
                if (this.x509Attribute.contains(",") && this.x509Attribute.toUpperCase().contains(SUBJECT_ALT_NAME.toUpperCase())
                        && this.x509Attribute.toUpperCase().contains(SUBJECTDN.toUpperCase())) {
                    if (clientCert.getSubjectAlternativeNames() != null) {
                        userName = getSubjectAlternativeName(clientCert.getSubjectAlternativeNames());
                    } else {
                        String subjectDN = clientCert.getSubjectX500Principal().getName();
                        logger.debug("The value for subjectDN  is  " + subjectDN);
                        userName = getUserNameFromPrincipal(subjectDN);
                    }
                }
            }
        } catch (CertificateParsingException cpe) {
            logger.debug("Exception while parsing certificate. " + cpe.getMessage());
        } catch (Exception e) {
            logger.error("Exception while extracting the user name from certificate. " + e.getMessage());
        }

        if (userName == null) {
            logger.info("Extraction of the username was not successful for the X.509 attribute " + this.x509Attribute);
        } else
            logger.info("Extracted user name is '" + userName + "'");

        return userName;
    }

    /**
     * This method returns the type of the subjectAlternativeName/issuerAlternative based on the value of sanType. For
     * example, if the sanType "rfc822Name", then the integer type returned is 1.
     * 
     * @return altNameIntegerType
     */
    private int fetchSubjectAltType() throws Exception {
        if (this.sanType.toUpperCase().startsWith(SubjectAlternativeNameGeneralNames.rfc822Name.name().toUpperCase()))
            return SubjectAlternativeNameGeneralNames.rfc822Name.ordinal();
        else if (this.sanType.toUpperCase().startsWith(SubjectAlternativeNameGeneralNames.otherName.name().toUpperCase()))
            return SubjectAlternativeNameGeneralNames.otherName.ordinal();

        throw new Exception("Subject Alternative Name of unsupported type (" + this.sanType
                + ") found. The supported types are othername and rfc822Name.");
    }

    /**
     * Helper method to extract the user name from subject DN
     * 
     * @param dn Subject DN
     * @return username
     */
    private String getUserNameFromPrincipal(String dn) {
        logger.debug(this.x509Attribute + " '" + dn + "'");

        Matcher matcher = getRegex().matcher(dn);

        if (!matcher.find()) {
            logger.debug("No matching pattern was found in " + this.x509Attribute + ": " + dn);
            throw new BadCredentialsException("No matching pattern was found in " + this.x509Attribute + ": " + dn);
        }

        if (matcher.groupCount() != 1) {
            logger.debug("Regular expression must contain a single group.");
            throw new IllegalArgumentException("Regular expression must contain a single group ");
        }

        String userName = matcher.group(1);

        logger.debug("The extracted username is " + userName);

        return userName;
    }

    /**
     * This method iterates through the list of subjectAlternativeNames and extracts the username from sanType which is
     * either 'rfc822Name' and 'OtherName'.
     * 
     * @param coll List of subjectAlternativeNames in the X.509Certificate.
     * @return Username. The extracted username from either rfc822Name or OtherName.
     */
    private String getSubjectAlternativeName(Collection<?> coll) throws Exception {
        String altName = null;
        boolean found = false;

        int altType = fetchSubjectAltType();
        Iterator<?> iter = coll.iterator();
        int OID = 0;

        while (iter.hasNext() && !found) {
            List<?> next = (List<?>) iter.next();
            OID = ((Integer) next.get(0)).intValue();

            if (OID == altType) {
                switch (OID) {
                    case 0:// SubjectAlternativeNameGeneralNames.otherName.ordinal():
                        Object otherNameExtensionField = next.get(1);
                        logger.debug("Found other name type.");
                        if (otherNameExtensionField != null) {
                            altName = otherNameUserPrincipalNameExtractor.determineUserPrincipalName(otherNameExtensionField);
                            logger.debug("Other name = " + altName);
                            if (altName != null) {
                                altName = altName.trim();
                            }

                            found = true;
                        } else {
                            logger.warn("The otherNameType field is null, no user principal extraction is being attempted.");
                        }

                        break;
                    case 1:// SubjectAlternativeNameGeneralNames.rfc822Name.ordinal():
                        altName = (String) next.get(1);
                        logger.debug("Found rfc822name = " + altName);
                        if (altName != null) {
                            altName = altName.trim();
                        }
                        found = true;

                        break;
                }
            }
        }

        if (!found) {
            logger.info("SubjectAltName of invalid type(" + OID + ") found.");
        }

        return altName;
    }

    /**
     * Sets the regular expression which will be used to extract the user name from the subjectDN attribute of X.509
     * certificate.
     * 
     * @param regex the regular expression to find in subjectDN.
     */
    public void setRegex(String regex) {
        Assert.hasText(regex, "Regular expression may not be null or empty");
        this.userNameRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
    }

    /**
     * Sets the x509 attribute name that value of which will be extracted to extract the user name from the X.509
     * certificate.
     * 
     * @param x509Attribute Name of the X.509 attribute
     */
    public void setX509Attribute(String x509Attribute) {
        this.x509Attribute = x509Attribute;
    }

    /**
     * Set the type of the subject alternative name, the value of which will be read to extract the user name.
     * 
     * @param sanType Name of the subject alternative name type ( othername or rfc822Name).
     */
    public void setSanType(String sanType) {
        this.sanType = sanType;
    }

    /**
     * Set the instance of the CustomX509UserPrincipalNameResolver used for extracting the user principal name.
     *
     * @param upnResolver object that extracts the user principal name from the certificate
     */
    public void setUPNResolver(CustomX509UserPrincipalNameExtractor upnResolver) {
        this.otherNameUserPrincipalNameExtractor = upnResolver;
    }

    private Pattern getRegex() {
        return this.userNameRegex;
    }

}